package vision

import (
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/photoprism/photoprism/internal/ai/vision/openai"
	"github.com/photoprism/photoprism/internal/ai/vision/schema"
	"github.com/photoprism/photoprism/internal/entity"
)

func TestOpenAIBuilderBuild(t *testing.T) {
	model := &Model{
		Type:   ModelTypeLabels,
		Name:   openai.DefaultModel,
		Engine: openai.EngineName,
	}
	model.ApplyEngineDefaults()

	request, err := openaiBuilder{}.Build(context.Background(), model, Files{examplesPath + "/chameleon_lime.jpg"})
	require.NoError(t, err)
	require.NotNil(t, request)

	assert.Equal(t, ApiFormatOpenAI, request.ResponseFormat)
	assert.NotEmpty(t, request.Images)
	assert.NotNil(t, request.Options)
	assert.Equal(t, openai.DefaultDetail, request.Options.Detail)
	assert.True(t, request.Options.ForceJson)
	assert.GreaterOrEqual(t, request.Options.MaxOutputTokens, openai.LabelsMaxTokens)
}

func TestOpenAIBuilderBuildCaptionDisablesForceJSON(t *testing.T) {
	model := &Model{
		Type:    ModelTypeCaption,
		Name:    openai.DefaultModel,
		Engine:  openai.EngineName,
		Options: &ModelOptions{ForceJson: true},
	}
	model.ApplyEngineDefaults()

	request, err := openaiBuilder{}.Build(context.Background(), model, Files{examplesPath + "/chameleon_lime.jpg"})
	require.NoError(t, err)
	require.NotNil(t, request)
	require.NotNil(t, request.Options)
	assert.False(t, request.Options.ForceJson)
	assert.GreaterOrEqual(t, request.Options.MaxOutputTokens, openai.CaptionMaxTokens)
}

func TestApiRequestJSONForOpenAI(t *testing.T) {
	req := &ApiRequest{
		Model:          "gpt-5-mini",
		System:         "system",
		Prompt:         "describe the scene",
		Images:         []string{""},
		ResponseFormat: ApiFormatOpenAI,
		Options: &ModelOptions{
			Detail:          openai.DefaultDetail,
			MaxOutputTokens: 128,
			Temperature:     0.2,
			TopP:            0.8,
			ForceJson:       true,
		},
		Schema: json.RawMessage(`{"type":"object","properties":{"caption":{"type":"object"}}}`),
	}

	payload, err := req.JSON()
	require.NoError(t, err)

	var decoded struct {
		Model string `json:"model"`
		Input []struct {
			Role    string `json:"role"`
			Content []struct {
				Type string `json:"type"`
			} `json:"content"`
		} `json:"input"`
		Text struct {
			Format struct {
				Type   string          `json:"type"`
				Name   string          `json:"name"`
				Schema json.RawMessage `json:"schema"`
				Strict bool            `json:"strict"`
			} `json:"format"`
		} `json:"text"`
		Reasoning struct {
			Effort string `json:"effort"`
		} `json:"reasoning"`
		MaxOutputTokens int `json:"max_output_tokens"`
	}

	require.NoError(t, json.Unmarshal(payload, &decoded))
	assert.Equal(t, "gpt-5-mini", decoded.Model)
	require.Len(t, decoded.Input, 2)
	assert.Equal(t, "system", decoded.Input[0].Role)
	assert.Equal(t, openai.ResponseFormatJSONSchema, decoded.Text.Format.Type)
	assert.Equal(t, schema.JsonSchemaName(decoded.Text.Format.Schema, openai.DefaultSchemaVersion), decoded.Text.Format.Name)
	assert.False(t, decoded.Text.Format.Strict)
	assert.NotNil(t, decoded.Text.Format.Schema)
	assert.Equal(t, "low", decoded.Reasoning.Effort)
	assert.Equal(t, 128, decoded.MaxOutputTokens)
}

func TestApiRequestJSONForOpenAIDefaultSchemaName(t *testing.T) {
	req := &ApiRequest{
		Model:          "gpt-5-mini",
		Images:         []string{""},
		ResponseFormat: ApiFormatOpenAI,
		Options: &ModelOptions{
			Detail:          openai.DefaultDetail,
			MaxOutputTokens: 64,
			ForceJson:       true,
		},
		Schema: json.RawMessage(`{"type":"object"}`),
	}

	payload, err := req.JSON()
	require.NoError(t, err)

	var decoded struct {
		Text struct {
			Format struct {
				Name string `json:"name"`
			} `json:"format"`
		} `json:"text"`
	}

	require.NoError(t, json.Unmarshal(payload, &decoded))
	assert.Equal(t, schema.JsonSchemaName(req.Schema, openai.DefaultSchemaVersion), decoded.Text.Format.Name)
}

func TestOpenAIParserParsesJSONFromTextPayload(t *testing.T) {
	respPayload := `{
		"id": "resp_123",
		"model": "gpt-5-mini",
		"output": [{
			"role": "assistant",
			"content": [{
				"type": "output_text",
				"text": "{\"labels\":[{\"name\":\"deer\",\"confidence\":0.98,\"topicality\":0.99}]}"
			}]
		}]
	}`

	req := &ApiRequest{
		Id:             "test",
		Model:          "gpt-5-mini",
		ResponseFormat: ApiFormatOpenAI,
	}

	resp, err := openaiParser{}.Parse(context.Background(), req, []byte(respPayload), http.StatusOK)
	require.NoError(t, err)
	require.NotNil(t, resp)
	require.Len(t, resp.Result.Labels, 1)
	assert.Equal(t, "Deer", resp.Result.Labels[0].Name)
	assert.Nil(t, resp.Result.Caption)
}

func TestParseOpenAISchemaLegacyUpgrade(t *testing.T) {
	legacy := `{
		"labels": [{
			"name": "",
			"confidence": 0,
			"topicality": 0
		}]
	}`

	raw, err := parseOpenAISchema(legacy)
	require.NoError(t, err)

	var decoded map[string]any
	require.NoError(t, json.Unmarshal(raw, &decoded))

	assert.Equal(t, "object", decoded["type"])

	props, ok := decoded["properties"].(map[string]any)
	require.True(t, ok)
	labels, ok := props["labels"].(map[string]any)
	require.True(t, ok)
	assert.Equal(t, "array", labels["type"])
}

func TestParseOpenAISchemaLegacyUpgradeNSFW(t *testing.T) {
	legacy := `{
		"labels": [{
			"name": "",
			"confidence": 0,
			"topicality": 0,
			"nsfw": false,
			"nsfw_confidence": 0
		}]
	}`

	raw, err := parseOpenAISchema(legacy)
	require.NoError(t, err)

	var decoded map[string]any
	require.NoError(t, json.Unmarshal(raw, &decoded))

	props := decoded["properties"].(map[string]any)
	labels := props["labels"].(map[string]any)
	items := labels["items"].(map[string]any)
	_, hasNSFW := items["properties"].(map[string]any)["nsfw"]
	assert.True(t, hasNSFW)
}

func TestPerformApiRequestOpenAISuccess(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		var reqPayload struct {
			Model string `json:"model"`
		}
		assert.NoError(t, json.NewDecoder(r.Body).Decode(&reqPayload))
		assert.Equal(t, "gpt-5-mini", reqPayload.Model)

		response := map[string]any{
			"id":    "resp_123",
			"model": "gpt-5-mini",
			"output": []any{
				map[string]any{
					"role": "assistant",
					"content": []any{
						map[string]any{
							"type": "output_json",
							"json": map[string]any{
								"caption": map[string]any{
									"text":       "A cat rests on a windowsill.",
									"confidence": 0.91,
								},
								"labels": []map[string]any{
									{
										"name":       "cat",
										"confidence": 0.92,
										"topicality": 0.88,
									},
								},
							},
						},
					},
				},
			},
		}

		assert.NoError(t, json.NewEncoder(w).Encode(response))
	}))
	defer server.Close()

	req := &ApiRequest{
		Id:             "test",
		Model:          "gpt-5-mini",
		Images:         []string{""},
		ResponseFormat: ApiFormatOpenAI,
		Options: &ModelOptions{
			Detail: openai.DefaultDetail,
		},
		Schema: json.RawMessage(`{"type":"object"}`),
	}

	resp, err := PerformApiRequest(req, server.URL, http.MethodPost, "secret")
	require.NoError(t, err)
	require.NotNil(t, resp)

	require.NotNil(t, resp.Result.Caption)
	assert.Equal(t, entity.SrcOpenAI, resp.Result.Caption.Source)
	assert.Equal(t, "A cat rests on a windowsill.", resp.Result.Caption.Text)

	require.Len(t, resp.Result.Labels, 1)
	assert.Equal(t, entity.SrcOpenAI, resp.Result.Labels[0].Source)
	assert.Equal(t, "Cat", resp.Result.Labels[0].Name)
}

func TestPerformApiRequestOpenAITextFallback(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		response := map[string]any{
			"id":    "resp_456",
			"model": "gpt-5-mini",
			"output": []any{
				map[string]any{
					"role": "assistant",
					"content": []any{
						map[string]any{
							"type": "output_text",
							"text": "Two hikers reach the summit at sunset.",
						},
					},
				},
			},
		}
		assert.NoError(t, json.NewEncoder(w).Encode(response))
	}))
	defer server.Close()

	req := &ApiRequest{
		Id:             "fallback",
		Model:          "gpt-5-mini",
		Images:         []string{""},
		ResponseFormat: ApiFormatOpenAI,
		Options: &ModelOptions{
			Detail: openai.DefaultDetail,
		},
		Schema: nil,
	}

	resp, err := PerformApiRequest(req, server.URL, http.MethodPost, "")
	require.NoError(t, err)
	require.NotNil(t, resp.Result.Caption)
	assert.Equal(t, "Two hikers reach the summit at sunset.", resp.Result.Caption.Text)
	assert.Equal(t, entity.SrcOpenAI, resp.Result.Caption.Source)
}

func TestPerformApiRequestOpenAIError(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusBadRequest)
		_ = json.NewEncoder(w).Encode(map[string]any{
			"error": map[string]any{
				"message": "Invalid image payload",
			},
		})
	}))
	defer server.Close()

	req := &ApiRequest{
		Id:             "error",
		Model:          "gpt-5-mini",
		ResponseFormat: ApiFormatOpenAI,
		Schema:         nil,
		Images:         []string{""},
	}

	_, err := PerformApiRequest(req, server.URL, http.MethodPost, "")
	require.Error(t, err)
	assert.Contains(t, err.Error(), "Invalid image payload")
}
